DivsGlobalVars
Global State vs. Dependency Injection in Go: Trade-offs and Best Practices
Introduction**
When structuring backend applications, one of the key decisions is whether to use global state or dependency injection (DI) for managing shared resources like database connections.
We are using a go backend as our example here, but of course this applies to any application.
Understanding Global State in Go**
What It Is
-
Defining variables at the package level:
package db
import (
"database/sql"
_ "github.com/lib/pq"
)
var DB *sql.DB
var Queries *Queries -
These variables are initialized once and shared across the application.
Pros of Using Global State
✅ Simplifies function signatures – No need to pass dependencies everywhere.
✅ Cleaner code in small projects – Reduces boilerplate.
✅ Good for truly global, single-instance resources like a database connection.
Cons of Using Global State
❌ Harder to test – You can't easily swap dependencies in tests.
- That said, I personally use
❌ Implicit dependencies – Functions rely on hidden state instead of explicit parameters.
❌ Risk of unintended side effects – If reinitialized, everything depending on it changes.
Understanding Dependency Injection in Go**
What It Is
- Instead of using package-level variables, dependencies are explicitly created and passed around:
package db
func NewDB() (*sql.DB, *Queries, error) {
db, err := sql.Open("postgres", "dsn_here")
if err != nil {
return nil, nil, err
}
queries := New(db)
return db, queries, nil
}package main
import "myapp/db"
func main() {
dbConn, queries, err := db.NewDB()
if err != nil {
log.Fatal(err)
}
startServer(dbConn, queries)
}func startServer(db *sql.DB, queries *Queries) {
handler := NewHandler(db, queries)
http.ListenAndServe(":8080", handler)
}
Pros of Dependency Injection
✅ Explicit dependencies – Clearer about what a function needs.
✅ Easier to test – Can inject mock implementations in unit tests. *See Note
✅ More flexible – Supports multiple instances (e.g., different DB connections).
Cons of Dependency Injection
❌ Function signatures become long – Need to pass dependencies explicitly.
❌ More boilerplate – Requires more struct initialization and passing.
When to Use Each Approach**
Scenario | Global State ✅ | Dependency Injection ✅ |
---|---|---|
Small apps, scripts | ✅ | 🚫 |
Large applications | 🚫 | ✅ |
Testing with mock databases | 🚫 | ✅ |
Keeping function signatures clean | ✅ | 🚫 |
Managing multiple DB instances | 🚫 | ✅ |
Conclusion**
- There’s no universal “right” choice—it depends on your project.
- Use global state for simplicity when it makes sense.
- Use dependency injection when you need testability and flexibility.
- Hybrid approaches can work, but be careful with global modifications.
- In your case, switching to global state helped clean up function signatures while keeping things manageable.
** A note on Testability * - I personally use E2E API call tests, since E2E tests can validate the entire flow. Therefore for typical CRUD backends i dont have unit tests for handlers. This makes dependency injection less critical for testability for this specific use case, but of course there may be other situations where I choose DI specifically for testability reasons.
This captures the trade-offs you’ve been working through in real-time. Want to add anything else before we refine it into a full article?